{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "ein.tags": "worksheet-0",
    "slideshow": {
     "slide_type": "-"
    }
   },
   "source": [
    "# 用户角色\n",
    "\n",
    "有多种方法可用于在程序中实现角色。具体采用何种实现方法取决于所需角色的数量和细分程度。\n",
    "\n",
    "本章介绍的用户角色实现方式结合了分立的角色和权限，赋予用户分立的角色，但角色使用权限定义。\n",
    "\n",
    "\n",
    "## 角色在数据库中的表示\n",
    "\n",
    "下面是改进后的`Role`模型，增加了角色的权限：\n",
    "\n",
    "```python\n",
    "# app/models.py\n",
    "class Role(db.Model):\n",
    "    name = pw.CharField(64, unique=True)\n",
    "    default = pw.BooleanField(default=False, index=True)\n",
    "    permissions = pw.IntegerField(null=True)\n",
    "```\n",
    "\n",
    "只有一个角色的`default`字段要设为`True`，其他都设为`False`。用户注册时，其角色会被设为默认角色。\n",
    "\n",
    "第二处改动是添加了`permissions`字段，其值是一个整数，表示位标志。各操作都对应一个位位置，能执行某项操作的角色，其位会被设为`1`。\n",
    "\n",
    "各操作所需的程序权限是不一样的，如下表所示：\n",
    "\n",
    "| 操作        | 位值                | 说明          |\n",
    "|-------------|---------------------|---------------|\n",
    "| 关注用户    | `0b00000001 (0x01)` | 关注其他用户  |\n",
    "| 在他人的文章中发表评论 | `0b00000010 (0x02)` | 在他人撰写的文章中发布评论 |\n",
    "| 写文章      | `0b00000100 (0x04)` | 写原创文章    |\n",
    "| 管理他人发表的评论 | `0b00001000 (0x08)` | 查处他人发表的不当评论 |\n",
    "| 管理员权限  | `0b10000000 (0x80)` | 管理网站      |\n",
    "\n",
    "⚠️ 操作的权限使用 8 位表示，现在只用了其中 5 位，其他 3 位可用于将来的扩充。\n",
    "\n",
    "使用如下的代码表示权限：\n",
    "\n",
    "```python\n",
    "# app/models.py\n",
    "class Permission:\n",
    "    FOLLOW = 0x01\n",
    "    COMMENT = 0x02\n",
    "    WRITE_ARTICLES = 0x04\n",
    "    MODERATE_COMMENTS = 0x08\n",
    "    ADMINISTER = 0x80\n",
    "```\n",
    "\n",
    "下表列出了要支持的用户角色以及定义角色使用的位权限：\n",
    "\n",
    "| 用户角色 | 权限                | 说明                             |\n",
    "|----------|---------------------|----------------------------------|\n",
    "| 匿名 | `0b00000000 (0x00)` | 未登录的用户。在程序中只有阅读权限 |\n",
    "| 用户 | `0b00000111 (0x07)` | 具有发布文章、发表评论和关注其他用户的权限。这是新用户的默认角色 |\n",
    "| 协管员 | `0b00001111 (0x0f)` | 增加审查不当评论的权限           |\n",
    "| 管理员 | `0b11111111 (0xff)` | 具有所有权限，包括修改其他用户所属角色的权限 |\n",
    "\n",
    "使用权限组织角色，以后添加新角色时只需使用不同的权限组合即可。\n",
    "\n",
    "在`Role`类中添加一个类方法用来将角色添加到数据库中：\n",
    "\n",
    "```python\n",
    "# app/models.py\n",
    "class Role(db.Model):\n",
    "    # ...\n",
    "    @staticmethod\n",
    "    def insert_roles():\n",
    "        roles = {\n",
    "            'User': (Permission.FOLLOW |\n",
    "                     Permission.COMMENT |\n",
    "                     Permission.WRITE_ARTICLES, True),\n",
    "            'Moderator': (Permission.FOLLOW |\n",
    "                          Permission.COMMENT |\n",
    "                          Permission.WRITE_ARTICLES |\n",
    "                          Permission.MODERATE_COMMENTS, False),\n",
    "            'Administrator': (0xff, False)\n",
    "        }\n",
    "        for r in roles:\n",
    "            role = Role.select().where(Role.name == r).first()\n",
    "            if role is None:\n",
    "                role = Role(name=r)\n",
    "            role.permissions = roles[r][0]\n",
    "            role.default = roles[r][1]\n",
    "            role.save()\n",
    "```\n",
    "\n",
    "`insert_roles()`函数先通过角色名查找现有的角色，然后再进行更新。只有当数据库中没有某个角色名时才会创建新角色对象。如果以后更新了角色列表，就可以执行更新操作。要想添加新角色，或者修改角色的权限，修改`roles`数组，再运行函数即可。\n",
    "\n",
    "使用 flask shell 会话将角色写入数据库：\n",
    "\n",
    "```python\n",
    "(flaskr_env3) $ flask shell\n",
    ">>> Role.insert_roles()\n",
    ">>> list(Role.select())\n",
    "[<Role 'User'>, <Role 'Moderator'>, <Role 'Administrator'>]\n",
    "```\n",
    "\n",
    "## 赋予角色\n",
    "\n",
    "用户在程序中注册账户时，即被赋予适当的角色。大多数用户在注册时赋予的角色都是 “用户”，即默认角色。管理员作为唯一的例外，应根据保存在设置变量`FLASKR_ADMIN`中 的电子邮件地址被赋予“管理员”角色。\n",
    "\n",
    "下面的代码用来定义默认的用户角色：\n",
    "\n",
    "```python\n",
    "# app/models.py\n",
    "class User(UserMixin, db.Model):\n",
    "    # ...\n",
    "    def __init__(self, **kwargs):\n",
    "        super(User, self).__init__(**kwargs)\n",
    "        if self.role is None:\n",
    "            if self.email == current_app.config['FLASKR_ADMIN']:\n",
    "                self.role = (Role.select()\n",
    "                             .where(Role.permissions == 0xff)\n",
    "                             .first())\n",
    "            if self.role is None:\n",
    "                self.role = Role.select().where(Role.default == True).first()\n",
    "```\n",
    "\n",
    "`User`类的构造函数首先调用基类的构造函数，如果创建基类对象后还没定义角色，则根据 电子邮件地址决定将其设为管理员还是默认角色。\n",
    "\n",
    "\n",
    "## 角色验证\n",
    "\n",
    "下面的代码用来检查用户是否有指定的权限：\n",
    "\n",
    "```python\n",
    "# app/models.py\n",
    "from flask_login import UserMixin, AnonymousUserMixin\n",
    "\n",
    "class User(UserMixin, db.Model):\n",
    "    # ...\n",
    "\n",
    "    def can(self, permissions):\n",
    "        return (self.role is not None and\n",
    "                (self.role.permissions & permissions) == permissions)\n",
    "\n",
    "    def is_administrator(self):\n",
    "        return self.can(Permission.ADMINISTER)\n",
    "\n",
    "\n",
    "class AnonymousUser(AnonymousUserMixin):\n",
    "    def can(self, permissions):\n",
    "        return False\n",
    "\n",
    "    def is_administrator(self):\n",
    "        return False\n",
    "\n",
    "\n",
    "login_manager.anonymous_user = AnonymousUser\n",
    "```\n",
    "\n",
    "`can()`方法在请求和赋予角色这两种权限之间进行 **位与操作** 。如果角色中包含请求的所有权限位，则返回`True`，表示允许用户执行此项操作。检查管理员权限的功能使用单独的方法`is_administrator()`实现。\n",
    "\n",
    "继承自 Flask-Login 中`AnonymousUserMixin`类的`AnonymousUser`类，也实现了`can()`方法和`is_administrator()`方法。通过将其设为用户未登录时`current_user`的值，程序不用先检查用户是否登录，也能自由调用`current_user.can()`和`current_user.is_administrator()`。\n",
    "\n",
    "如果想让视图函数只对具有特定权限的用户开放，可以使用自定义的装饰器：\n",
    "\n",
    "```python\n",
    "# app/decorators.py\n",
    "from functools import wraps\n",
    "\n",
    "from flask import abort\n",
    "from flask_login import current_user\n",
    "\n",
    "from .models import Permission\n",
    "\n",
    "\n",
    "def permission_required(permission):\n",
    "    def decorator(f):\n",
    "        @wraps(f)\n",
    "        def decorated_function(*args, **kwargs):\n",
    "            if not current_user.can(permission):\n",
    "                abort(403)\n",
    "            return f(*args, **kwargs)\n",
    "        return decorated_function\n",
    "    return decorator\n",
    "\n",
    "\n",
    "def admin_required(f):\n",
    "    return permission_required(Permission.ADMINISTER)(f)\n",
    "```\n",
    "\n",
    "上面代码实现了两个装饰器，一个用来检查常规权限，一个专门用来检查管理员权限。如果用户不具有指定权限，则返回 **403** 错误码，即HTTP“禁止”错误。\n",
    "\n",
    "下面的代码演示如何使用上面的装饰器：\n",
    "\n",
    "```python\n",
    "from decorators import admin_required, permission_required\n",
    "from .models import Permission\n",
    "\n",
    "@main.route('/admin')\n",
    "@login_required\n",
    "@admin_required\n",
    "def for_admins_only():\n",
    "    return \"For administrators!\"\n",
    "\n",
    "@main.route('/moderator')\n",
    "@login_required\n",
    "@permission_required(Permission.MODERATE_COMMENTS)\n",
    "def for_moderators_only():\n",
    "    return \"For comment moderators!\"\n",
    "```\n",
    "\n",
    "为了方便在模板中检查权限时使用`Permission`类，避免每次调用`render_template()`时都多添加一个模板参数，可以使用 **上下文处理器** 。 **上下文处理器** 能让变量在所有模板中全局可访问。\n",
    "\n",
    "下面的代码用来把`Permission`类加入模板上下文：\n",
    "\n",
    "```python\n",
    "# app/main/__init__.py\n",
    "from ..models import Permission\n",
    "\n",
    "\n",
    "@main.app_context_processor\n",
    "def inject_permissions():\n",
    "    return dict(Permission=Permission)\n",
    "```\n",
    "\n",
    "另外，新添加的角色和权限可在单元测试中进行测试。\n",
    "\n",
    "**🔖 执行`git checkout 9a`签出程序的这个版本。** ⚠️ 此版本包含一个数据库迁移。\n",
    "\n",
    "⚠️ 最好重新创建或更新开发数据库，为了赋予角色给在实现角色和权限之前创建的用户。"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "name": "9-user-roles.ipynb"
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
